package com.stark.commons.lang.util;

import com.alibaba.druid.filter.config.ConfigTools;
import com.github.rjeschke.txtmark.Processor;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.FluentPropertyBeanIntrospector;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.LineIterator;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.beans.PropertyDescriptor;
import java.io.*;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

/**
 * 公共方法集合。
 * @author Ben
 * @since 1.0.0
 * @version 1.0.0
 */
@Slf4j
public class Utils {
	
	/**
	 * 生成一个 32 位的 UUID 。
	 * @return uuid 。
	 */
	public static String createUUID() {
        return UUID.randomUUID().toString().replace("-", "");
	}

	/**
	 * 格式化货币字符串，每三位用 "," 隔开并保留两位有效数字。
	 * @param num 数值。
	 * @return 格式化的字符串。
	 */
	public static String formatMoney(Number num) {
		DecimalFormat formatter = new DecimalFormat(",##0.00");
        return formatter.format(num);
	}

	/**
	 * 格式化货币字符串，每三位用 "," 隔开并保留 deci 位有效数字。
	 * @param num 数值。
	 * @param deci 有效数字长度。
	 * @return 格式化的字符串。
	 */
	public static String formatMoney(Number num, int deci) {
		StringBuilder str = new StringBuilder(",##0" + (deci > 0 ? "." : ""));
		for (int i = 0; i < deci; i++) {
			str.append("0");
		}
		DecimalFormat formatter = new DecimalFormat(str.toString());
		return formatter.format(num);
	}

	/**
	 * 月、日、时、分、秒小于 10 时前补 0 。
	 * @param num 数值。
	 * @return 前补 0 后的字符串。
	 */
	public static String prependZero(int num) {
		return (num >= 10 ? "" : "0") + num;
	}

	/**
	 * 获取客户端 ip 地址。
	 * @param request HTTP 请求对象。
	 * @return 客户端 ip 地址。
	 */
	public static String getClientIp(HttpServletRequest request) {
		if (request == null) {
			return null;
		}
		// nginx
		String ip = request.getHeader("X-Real-IP");
		if (StringUtils.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
			return ip;
		}
		// apache proxy
		ip = request.getHeader("Proxy-Client-IP");
		if (StringUtils.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
			return ip;
		}
		// weblogic proxy
		ip = request.getHeader("WL-Proxy-Client-IP");
		if (StringUtils.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
			return ip;
		}
		// X-Forwarded-For
		ip = request.getHeader("X-Forwarded-For");
		if (StringUtils.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
			String[] array = ip.replace(" ", "").split(",");
			for (String item : array) {
				if (StringUtils.isNotBlank(item)) {
					return item;
				}
			}
		}
		// no proxy
		return request.getRemoteAddr();
	}
	
	/**
	 * gzip 压缩字符串。
	 * @param str 字符串。
	 * @return 压缩后的字符串。
	 * @throws IOException 压缩过程中抛出异常。
	 */
	public static String gzipStr(String str) throws IOException {
		if (StringUtils.isBlank(str)) {
			return "";
		}

		@Cleanup ByteArrayOutputStream out = new ByteArrayOutputStream();
		@Cleanup GZIPOutputStream gzip = new GZIPOutputStream(out);
		gzip.write(str.getBytes(StandardCharsets.UTF_8));
		gzip.finish();
		out.flush();
		str = Base64.encodeBase64String(out.toByteArray());
		return str;
	}

	/**
	 * gzip 解压缩字符串。
	 * @param str 字符串。
	 * @return 解压后的字符串。
	 * @throws IOException 解压过程中抛出异常。
	 */
	public static String ungzipStr(String str) throws IOException {
		if (StringUtils.isBlank(str)) {
			return "";
		}

		@Cleanup InputStream in = new ByteArrayInputStream(Base64.decodeBase64(str));
		@Cleanup GZIPInputStream gzip = new GZIPInputStream(in);
		@Cleanup ByteArrayOutputStream out = new ByteArrayOutputStream();
		byte[] buffer = new byte[1024];
		int offset;
		while ((offset = gzip.read(buffer)) != -1) {
			out.write(buffer, 0, offset);
		}
		out.flush();
		str = new String(out.toByteArray(), StandardCharsets.UTF_8);
		return str;
	}
	
	/**
	 * 获得 url 参数 map 。
	 * @param queryString url 查询参数字符串。
	 * @return 参数 map 。
	 */
	public static Map<String, String> getQueryParamMap(String queryString) {
		Map<String, String> map = new HashMap<>();
		if (StringUtils.isBlank(queryString)) {
			return map;
		}
		
		String[] paramArray = queryString.split("&");
		for (String keyValueStr : paramArray) {
			map.put(StringUtils.substringBefore(keyValueStr, "="), StringUtils.substringAfter(keyValueStr, "="));
		}
		return map;
	}
	
	/**
	 * 获得 url 参数 map 。
	 * @param request HTTP 请求对象。
	 * @return 参数 map 。
	 * @see #getQueryParamMap(String)
	 */
	public static Map<String, String> getQueryParamMap(HttpServletRequest request) {
		String queryString = request.getQueryString();
        return getQueryParamMap(queryString);
	}
	
	/**
	 * 根据 url 参数 map 获得查询字符串。
	 * @param queryParamMap 参数 map 。
	 */
	public static String getQueryString(Map<String, String> queryParamMap) {
		if (MapUtils.isEmpty(queryParamMap)) {
			return "";
		}

        return queryParamMap.entrySet()
			.stream()
			.filter(entry -> StringUtils.isNotBlank(entry.getKey()) && StringUtils.isNotBlank(entry.getValue()))
			.map(entry -> {
				try {
					return entry.getKey() + "=" + URLEncoder.encode(entry.getValue(), "UTF-8");
				} catch (UnsupportedEncodingException e) {
					throw new RuntimeException(e);
				}
			})
			.reduce("", (s1, s2) -> s1 + (s1.isEmpty() ? "" : "&") + s2);
	}
	
	/**
	 * 获取下载文件名。
	 * @param response HTTP 响应对象。
	 * @return 文件名。
     */
	public static String getFilename(HttpServletResponse response) throws UnsupportedEncodingException {
		String contentDisposition = response.getHeader("Content-Disposition");
		if (StringUtils.isNotBlank(contentDisposition)) {
			String filename = StringUtils.substringAfter(contentDisposition, "filename=");
			filename = new String(filename.getBytes("ISO8859-1"), StandardCharsets.UTF_8);
			return filename;
		}
		return "";
	}
	
	/**
	 * 加密 druid 连接池密码，在控制台打印：
	 * <ul>
	 * <li>privateKey - 私钥</li>
	 * <li>publicKey - 公钥</li>
	 * <li>password - 加密后的密码</li>
	 * </ul>
	 * @param password 密码明文。
	 * @throws Exception 加密时抛出异常。
	 * @see com.alibaba.druid.filter.config.ConfigTools#main(String[])
	 */
	public static void encryptDruidPassword(String password) throws Exception {
		ConfigTools.main(new String[] { password });
	}
	
	/**
	 * 解密 druid 连接池密码。
	 * @param password 加密字符串。
	 * @return 解密后的密码明文。
	 * @throws Exception 解密时抛出异常。
	 * @see com.alibaba.druid.filter.config.ConfigTools#decrypt(String, String)
	 */
	public static String decryptDruidPassword(String publicKey, String password) throws Exception {
		password = ConfigTools.decrypt(publicKey, password);
		return password;
	}
	
	/**
	 * 更改指定文件的字符编码。
	 * @param file 文件对象
	 * @param sourceEncoding 源编码。
	 * @param destEncoding 目的编码。
     */
	public static void changeFileEncoding(File file, String sourceEncoding, String destEncoding)
			throws IOException {
		String str = FileUtils.readFileToString(file, sourceEncoding);
		byte[] bytes = str.getBytes(destEncoding);
		FileUtils.write(file, new String(bytes, destEncoding), Charset.defaultCharset());
		log.debug("文件转码成功：{}，{}转{}", file.getAbsolutePath(), sourceEncoding, destEncoding);
	}
	
	/**
	 * 更改指定文件的字符编码。
	 * @param filename 文件路径。
	 * @param sourceEncoding 源编码。
	 * @param destEncoding 目的编码。
	 * @see #changeFileEncoding(File, String, String)
	 */
	public static void changeFileEncoding(String filename, String sourceEncoding, String destEncoding)
			throws IOException {
		changeFileEncoding(new File(filename), sourceEncoding, destEncoding);
	}
	
	/**
	 * 更改指定路径下所有文件的字符编码。
	 * @param dir 指定路径。
	 * @param sourceEncoding 源编码。
	 * @param destEncoding 目的编码。
	 * @see #changeFileEncoding(File, String, String)
	 */
	public static void changeFilesEncoding(File dir, String sourceEncoding, String destEncoding) throws IOException {
		File[] files = dir.listFiles();
		if (files == null) {
			return;
		}
		for (File file : files) {
			if (file.isDirectory()) {
				changeFilesEncoding(file, sourceEncoding, destEncoding);
			} else if (file.isFile()) {
				changeFileEncoding(file, sourceEncoding, destEncoding);
			}
		}
	}
	
	/**
	 * 获得 Javabean 属性的 get 或 set 方法名。
	 * @param getOrSet 方法类型， "get" 或 "set" 。
	 * @param fieldName 属性名称。
	 * @return 方法名。
	 */
	public static String camelCaseGetterSetter(String getOrSet, String fieldName) {
		if (StringUtils.isBlank(fieldName)) {
			return "";
		}
		
		String funcName;
		if (fieldName.length() <= 1) {
			funcName = getOrSet + fieldName.toUpperCase();
			return funcName;
		}
		
		String secondChar = fieldName.length() == 2 ? fieldName.substring(1) : fieldName.substring(1, 2);
		if (StringUtils.isAllUpperCase(secondChar)) {
			funcName = getOrSet + fieldName.charAt(0) +  fieldName.substring(1);
			return funcName;
		}
		
		funcName = getOrSet + fieldName.substring(0, 1).toUpperCase() +  fieldName.substring(1);
		return funcName;
	}
	
	/**
	 * 获取当前服务器 ip 。
	 * @return 服务器 ip 。 
	 * @throws IOException 主机无法识别时抛出异常。
	 */
	public static String getServerIp() throws IOException {
		InetAddress addr = null;
		Enumeration<NetworkInterface> netInterfaces = NetworkInterface.getNetworkInterfaces();
		NetworkInterface netInterface;
		Enumeration<InetAddress> addresses;
		InetAddress _addr;
		while (netInterfaces.hasMoreElements()) {
			netInterface = netInterfaces.nextElement();
			addresses = netInterface.getInetAddresses();
			while (addresses.hasMoreElements()){
				_addr = addresses.nextElement();
				if (_addr instanceof Inet4Address
                        && !_addr.isLoopbackAddress()
                        && !_addr.getHostAddress().contains(":")) {
					addr = _addr;
					break;
				} 
			}
		}
		
		return addr != null ? addr.getHostAddress() : "";
	}
	
	/**
	 * 获取当前服务器主机名。
	 * @return 服务器主机名。
	 * @throws IOException 主机无法识别时抛出异常。
	 */
	public static String getServerHostName() throws IOException {
		InetAddress addr = null;
		Enumeration<NetworkInterface> netInterfaces = NetworkInterface.getNetworkInterfaces();
		NetworkInterface netInterface;
		Enumeration<InetAddress> addresses;
		InetAddress _addr;
		while (netInterfaces.hasMoreElements()) {
			netInterface = netInterfaces.nextElement();
			addresses = netInterface.getInetAddresses();
			while (addresses.hasMoreElements()){
				_addr = addresses.nextElement();
				if (_addr instanceof Inet4Address
                        && !_addr.isLoopbackAddress()
                        && !_addr.getHostAddress().contains(":")) {
					addr = _addr;
					break;
				} 
			}
		}
		
		return addr != null ? addr.getHostName() : "";
	}
	
	/**
	 * 在 URL 后拼接查询字符串。
	 * @param url 请求 URL 。
	 * @param queryString 参数字符串。
	 * @return 拼接所得的字符串。
	 */
	public static String concatURLQueryString(String url, String queryString) {
		if (StringUtils.isBlank(url)) {
			return "";
		}
		
		if (StringUtils.isBlank(queryString)) {
			return url;
		}
		
		if (!url.endsWith("?") && !url.endsWith("&")) {
			url += (url.contains("?") ? "&" : "?");
		}
		
		if ((queryString.startsWith("?") || queryString.startsWith("&")) && queryString.length() > 1) {
			queryString = queryString.substring(1);
		}
		
		url += queryString;
		return url;
	}
	
	/**
	 * Javabean 转 HashMap 。
	 * @param javabean Javabean 。
	 * @return {@link HashMap}
     */
	public static Map<String, Object> beanToMap(Object javabean) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 
        Map<String, Object> params = new HashMap<>(0);
        PropertyUtilsBean propertyUtilsBean = new PropertyUtilsBean();
        PropertyDescriptor[] descriptors = propertyUtilsBean.getPropertyDescriptors(javabean);
        String name;
        for (PropertyDescriptor descriptor : descriptors) {
            name = descriptor.getName();
            if (!"class".equals(name)) {
                params.put(name, propertyUtilsBean.getNestedProperty(javabean, name));
            }
        }
        return params;
	}
    
	/**
	 * 判断字符串是否为数值类型。
	 * @param str 字符串。
	 * @return 以下格式返回 true：
	 * <p>"123", "0123", "+123", "-123", "+0123", "-0123"
	 * <p>"0.1", "+0.1", "-0.1"
	 * <p>"0.12%", "+0.12%", "-0.12%"
	 */
	public static boolean isNumeric(String str) {
		if (StringUtils.isBlank(str)) {
			return false;
		}
		
		if (str.endsWith("%")) {
			str = str.substring(0, str.length() - 1);
		}
		
		try {
			Double.parseDouble(str.trim());
			return true;
		} catch (Exception e) {
			return false;
		}
	}
	
	/**
	 * 字符串转换成数值类型。
	 * @param str 字符串。
	 * @return <p>"123", "0123", "+123", "+0123" 返回 123.0
	 * <p>"-123", "-0123" 返回 -123.0
	 * <p>"0.1", "+0.1" 返回 0.1
	 * <p>"-0.1" 返回 -0.1
	 * <p>"0.12%", "+0.12%" 返回 0.0012
	 * <p>"-0.12%" 返回 -0.0012
	 * @throws ParseException 无法转化数值类型时抛出异常。
	 */
	public static Double numberValueOf(String str) throws ParseException {
		double x = 1;
		if (StringUtils.isNotBlank(str) && str.endsWith("%")) {
			str = str.substring(0, str.length() - 1);
			x = 0.01;
		}
		
		try {
			double num = Double.parseDouble(str);
			return num * x;
		} catch (Exception e) {
			throw new ParseException(e.getMessage(), 0);
		}
	}
	
	/**
	 * 获取异常的堆栈跟踪信息。
	 * @param e 异常对象。
	 * @return 堆栈跟踪信息。
	 */
	public static String getExceptionStackTrace(Exception e) {
		StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));
        return sw.toString().replaceAll("\n", "<br>&emsp;");
	}
	
	/**
	 * 执行本地 Windows 批处理程序。
	 * @param batPath 批处理程序绝对路径。
	 * @param out 结果输出流。
	 * @param encoding 字符编码。
	 * @param lineSeparator 换行符。
	 * @throws IOException 指定路径不存在时抛出异常。
	 * @throws InterruptedException 进程异常中断时抛出异常。
	 */
	public static void execBat(String batPath, final OutputStream out, final String encoding, final String lineSeparator)
			throws IOException, InterruptedException {
		IOUtils.write("批处理程序 " + batPath + " 开始执行 ..." + lineSeparator, out, encoding);
		final CountDownLatch countDownLatch = new CountDownLatch(2);
		
		final Process process = Runtime.getRuntime().exec(batPath);
		// 1. 处理进程标准输出信息流
		process.getOutputStream().close();
		// 2. 处理进程错误输出信息流
		new Thread(() -> {
			InputStream input = null;
			try {
				input = process.getErrorStream();
				String error = IOUtils.toString(input, "GBK");
				if (StringUtils.isNotBlank(error)) {
					IOUtils.write("捕捉到错误信息： " + error + lineSeparator, out, encoding);
				}
				countDownLatch.countDown();
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				if (input != null) {
					try {
						input.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}
		}).start();
		// 3. 处理进程输入流
		new Thread(() -> {
			InputStream input = null;
			try {
				input = process.getInputStream();
				LineIterator iter = IOUtils.lineIterator(input, "GBK");
				while (iter.hasNext()) {
					IOUtils.write(iter.nextLine() + lineSeparator, out, encoding);
				}
				countDownLatch.countDown();
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				if (input != null) {
					try {
						input.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}
		}).start();
		
		countDownLatch.await();
		IOUtils.write("批处理程序 " + batPath + " 执行完毕。" + lineSeparator, out, encoding);
	}
	
	/**
	 * 执行本地 Windows 批处理程序。
	 * @param batPath 批处理程序绝对路径。
	 * @throws IOException 指定路径不存在时抛出异常。
	 * @throws InterruptedException 进程异常中断时抛出异常。
	 */
	public static void execBat(String batPath) throws IOException, InterruptedException {
		log.debug("批处理程序 {} 开始执行 ...", batPath);
		final CountDownLatch countDownLatch = new CountDownLatch(2);
		
		final Process process = Runtime.getRuntime().exec(batPath);
		// 1. 处理进程标准输出信息流
		process.getOutputStream().close();
		// 2. 处理进程错误输出信息流
		new Thread(() -> {
			InputStream input = null;
			try {
				input = process.getErrorStream();
				String error = IOUtils.toString(input, "GBK");
				if (StringUtils.isNotBlank(error)) {
					log.error("捕捉到错误信息： {}", error);
				}
				countDownLatch.countDown();
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				if (input != null) {
					try {
						input.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}
		}).start();
		// 3. 处理进程输入流
		new Thread(() -> {
			InputStream input = null;
			try {
				input = process.getInputStream();
				String msg = IOUtils.toString(input, "GBK");
				log.debug(msg);
				countDownLatch.countDown();
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				if (input != null) {
					try {
						input.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}
		}).start();
		
		countDownLatch.await();
		log.debug("批处理程序 {} 执行完毕。", batPath);
	}
	
	/**
	 * markdown 转 html 。
	 * @param mdText markdown 文件内容。
	 * @return html 格式的 markdown 文件内容。
	 */
	public static String markdown2html(String mdText) {
		String mdHtml = Processor.process(mdText);
		return mdHtml.replace("<a", "<a target=\"_blank\"");
	}
	
	/**
	 * 判断是否是子类。
	 * @param clazz 父类。
	 * @param typeName 类名。
	 * @return 是返回 {@literal true} ，否则返回 {@literal false} 。
	 */
	public static boolean isAssignableFrom(Class<?> clazz, String typeName) {
		try {
			return clazz.isAssignableFrom(Class.forName(typeName));
		} catch (Exception e) {
			return false;
		}
	}
	
	/**
	 * 根据子类对象获取父类对象。
	 * @param <T> 父类类型。
	 * @param object 子类对象。
	 * @param superclass 父类。
	 * @return 父类对象。
	 */
	public static <T> T instanceSuperclass(Object object, Class<T> superclass) {
		if (!superclass.isAssignableFrom(object.getClass())) {
			throw new RuntimeException("[" + superclass.getName() + "] is not assignable from [" + object.getClass().getName() + "]");
		}
		
		try {
			T parent = superclass.getDeclaredConstructor().newInstance();
			PropertyUtils.addBeanIntrospector(new FluentPropertyBeanIntrospector());
			List<Field> fields = FieldUtils.getAllFieldsList(superclass);
			fields.forEach(field -> {
				try {
					Object value = PropertyUtils.getProperty(object, field.getName());
					PropertyUtils.setProperty(parent, field.getName(), value);
				} catch (NoSuchMethodException e) {
					// do nothing
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			});
			return parent;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
	
	/**
	 * 判断是否是 java 原生的类。
	 * @param clazz 类。
	 * @return 是返回 {@literal true} ，否则返回 {@literal false} 。
	 */
	public static boolean isNativeClass(Class<?> clazz) {
		return clazz != null && clazz.getClassLoader() == null;
	}
	
	/**
	 * 获取进程号。
	 * @return 进程号。
	 */
	public static int getPID() {
		RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
        String name = runtime.getName();
        return Integer.parseInt(StringUtils.substringBefore(name, "@"));
	}
	
}
